package cs536.staticSemantics.types;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import cs536.ast.AstVisitor;
import cs536.ast.AbstractSyntaxTree;
import static cs536.ast.AbstractSyntaxTree.*;

public class TypeChecker extends AstVisitor<Type>
{
	// data members
	private boolean passed = true;
	
	// previsit type
	private Type currFuncType;
	private String name;
	
	public boolean passed() {
		return passed;
	}



	public Type visitGlobalVarDecl(GlobalVarDecl globalVar,
			Type typeResult, 
			Type exprResult)
	{
//		if( !(typeResult.getKind().equals(exprResult.getKind()))) {
//			System.out.println(globalVar.getLocation().getRow() + ":" + 
//					globalVar.getLocation().getCol() + ":" +
//					"Type mismatch");	
//			return ErrorType.make();
//		}

		return typeResult;
	}


	public Type visitAst(AbstractSyntaxTree ast,
			List<Type> declarationResults)
	{		
//		Iterator<Type> iter = declarationResults.iterator();
//		Type temp;
//		
//		
//		while (iter.hasNext()) {
//			temp = iter.next();
//			if (temp == null) {
//				//passed = false;
//				return temp;
//			}
//			if (temp.getKind().equals("error")) {
//				passed = false;
//				return temp;
//			}
//		}
		
		// for now just return an int type if visitAst is successful
		return null;
		
	}

	public void preVisit(FuncDecl funcDecl) {
		// wanna grab the function type for later
		if (funcDecl.getReturnTypeNode() == null) {
			currFuncType = NullType.make();
		} else {
			currFuncType = funcDecl.getReturnTypeNode().asSemanticType();
		}
	}

	public Type visitFuncDecl(FuncDecl funcDecl, 
			Type typeResult,
			List<Type> formalResults, 
			List<Type> statementResults)
	{
		return typeResult;
	}

	public Type visitFormalArg(FormalArg formalArg, Type typeResult)
	{
		return typeResult;
	}

	public Type visitVarDeclStmt(VarDeclStmt localVar,
			Type typeResult, 
			Type initResult)
	{
		if (initResult != null) {
			if((!typeResult.getKind().equals(initResult.getKind()) || 
				(typeResult.getKind().equals("int") && initResult.getKind().equals("double"))) &&
				!(typeResult.getKind().equals("pointer") && initResult.getKind().equals("null"))) {
				System.out.println(localVar.getLocation().getRow() + ":" + 
						localVar.getLocation().getCol() + ":" +
						"Type mismatch");
				passed = false;
				return ErrorType.make();

			}

			if(typeResult.getKind().equals("error") || initResult.getKind().equals("error")) {
				return ErrorType.make();
			}		
		}

		//localVar.getLocalSymbol().;
		return typeResult;
	}


	public Type visitReturnStmt(ReturnStmt retStmt, Type valResult)
	{
		//System.out.println(currFuncType.getKind());
		if (currFuncType.getKind().equals("null") && valResult != null) {
			System.out.println(retStmt.getLocation().getRow() + ":" + retStmt.getLocation().getCol() + 
					":Returning a value from a void function");
			passed = false;
			return ErrorType.make();
		} 
		
		if ((!currFuncType.getKind().equals("null")) && (valResult == null)) {
			System.out.println(retStmt.getLocation().getRow() + ":" + retStmt.getLocation().getCol() + 
					":Missing return value");
			//System.out.println(currFuncType.getKind());
			passed = false;
			return ErrorType.make();
		}
		
		if (valResult != null && !valResult.getKind().equals(currFuncType.getKind())) {
			System.out.println(retStmt.getLocation().getRow() + ":" + retStmt.getLocation().getCol() + 
					":Bad return type");
			passed = false;
			return ErrorType.make();
		} 

		// want to return null if there's no valResult
		if (valResult != null) {
			retStmt.getExpression().setType(valResult);
			return valResult;
		} else {
			return NullType.make();
		}
	}


	public Type visitIfStmt(IfStmt ifStmt,
			Type conditionResult,
			List<Type> thenResults,
			List<Type> elseResults)
	{
		String kind = conditionResult.getKind();
		
		if ((!kind.equals("bool")) && (!kind.equals("error"))) {
			System.out.println(ifStmt.getLocation().getRow() + ":" + 
					ifStmt.getLocation().getCol() + ":" + 
					"Non-boolean expression used as a condition");
			passed = false;
			return ErrorType.make();
		}
		
		/*
		 * return error type if there is an error in the lists given
		 */
		
		Iterator<Type> thenIter = thenResults.iterator();
		Iterator<Type> elseIter = elseResults.iterator();
		Type temp;
		
		while(thenIter.hasNext()) {
			temp = thenIter.next();
			if(temp.getKind().equals("error")) {
				return ErrorType.make();
			}
		}
		
		while(elseIter.hasNext()) {
			temp = elseIter.next();
			if(temp.getKind().equals("error")) {
				return ErrorType.make();
			}
		}			
		
		return conditionResult;
	}


	public Type visitWhileStmt(WhileStmt whileStmt,
			Type conditionResult,
			List<Type> bodyResults)
	{
		String kind = conditionResult.getKind();
		
		if ((!kind.equals("bool")) && (!kind.equals("error"))) {
			System.out.println(whileStmt.getLocation().getRow() + ":" + 
					whileStmt.getLocation().getCol() + ":" + 
					"Non-boolean expression used as a condition");
			passed = false;
			return ErrorType.make();
		}
		
		/*
		 * return error type if there is an error in the lists given
		 */
		Iterator<Type> bodyIterator = bodyResults.iterator();
		Type temp;
		
		while(bodyIterator.hasNext()) {
			temp = bodyIterator.next();
			if(temp.getKind().equals("error")) {
				return ErrorType.make();
			}
		}
		
		return conditionResult;
	}


	public Type visitExpressionStmt(ExpressionStmt exprStmt,
			Type expResult)
	{
		exprStmt.getExpression().setType(expResult);
		return expResult;
	}


	public Type visitWriteStmt(WriteStmt writeStmt, Type valResult)
	{
		writeStmt.getExpression().setType(valResult);
		return valResult;
	}


	public Type visitReadStmt(ReadStmt readStmt)
	{
		return null;
	}


	public Type visitFunctionCallExp(FunctionCallExp fcallExp,
			List<Type> argResults)
	{
		// The name of the function to call is accessible through
		// fcallExp
		//Symbol name = fcallExp.ge

		return fcallExp.getType();
	}


	public Type visitDerefOp(DerefOp derefOp, Type expResult)
	{
		
		derefOp.makelvalue();
		
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		
		if(!expResult.getKind().equals("pointer")) {
			System.out.println(derefOp.getLocation().getRow() + ":" + 
					derefOp.getLocation().getCol() + ":" +
					"Applying the dereference operator to a non-pointer type.");
			return ErrorType.make();
		}
		return expResult;
	}


	public Type visitNegationOp(NegationOp negationOp, Type expResult)
	{
		
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		
		if (!(expResult.getKind().equals("int") || expResult.getKind().equals("double"))) {
			System.out.println(negationOp.getLocation().getRow() + ":" + 
					negationOp.getLocation().getCol() + ":" +
					"Attempting to negate something that isn't a number");
			return ErrorType.make();
		}

		return expResult;
	}

	public Type visitIncrementOp(IncrementOp incrOp, Type expResult)
	{
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		
		if (!(expResult.getKind().equals("int") || expResult.getKind().equals("double") || expResult.getKind().equals("pointer"))) {
			System.out.println(incrOp.getLocation().getRow() + ":" + 
					incrOp.getLocation().getCol() + ":" +
					"Integer arithmetic operator applied to non-numeric, non-pointer type");
			return ErrorType.make();
		}

		return expResult;
	}


	public Type visitDecrementOp(DecrementOp decrOp, Type expResult)
	{
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		
		if (!(expResult.getKind().equals("int") || expResult.getKind().equals("double") || expResult.getKind().equals("pointer"))) {
			System.out.println(decrOp.getLocation().getRow() + ":" + 
					decrOp.getLocation().getCol() + ":" +
					"Integer arithmetic operator applied to non-numeric, non-pointer type");
			return ErrorType.make();
		}

		return expResult;
	}


	public Type visitNotOp(NotOp logNotOp, Type expResult)
	{
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		
		if (!expResult.getKind().equals("bool")) {
			System.out.println(logNotOp.getLocation().getRow() + ":" + 
					logNotOp.getLocation().getCol() + ":" +
					"Applying a logical operator to an operand of type other than bool");
			return ErrorType.make();
		}

		return expResult;
	}


	public Type visitAddrOfOp(AddrOfOp addrOfOp, Type expResult)
	{
		if(expResult.getKind().equals("error")) {
			return expResult;
		}
		return PointerType.make(expResult);
	}

	public Type visitArrayAccessOp(ArrayAccessOp arrayAccessOp,
			Type lhsResult,
			Type rhsResult)
	{
		
		arrayAccessOp.makelvalue();
		
		return null;
	}


	public Type visitAndOp(AndOp logAndOp,
			Type lhsResult,
			Type rhsResult)
	{		
		
		if(!lhsResult.getKind().equals("bool") || !rhsResult.getKind().equals("bool")) {
			System.out.println(logAndOp.getLocation().getRow() + ":" + 
					logAndOp.getLocation().getCol() + ":" +
					"Logical operator applied to non-bool operand");	
			return ErrorType.make();
			
		}
		
		if(lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		
		return lhsResult;
	}

	public Type visitOrOp(OrOp logOrOp, Type lhsResult, Type rhsResult)
	{		
		
		if(!lhsResult.getKind().equals("bool") || !rhsResult.getKind().equals("bool")) {
			System.out.println(logOrOp.getLocation().getRow() + ":" + 
					logOrOp.getLocation().getCol() + ":" +
					"Logical operator applied to non-bool operand");	
			return ErrorType.make();
			
		}
		
		if(lhsResult.getKind().equals("error")) {
			return rhsResult;
		}	
		
		
		return lhsResult;
	}


	public Type visitAssignOp(AssignOp assignOp,
			Type lhsResult,
			Type rhsResult)
	{		
		System.out.println("rhs: " + rhsResult.getKind());
		System.out.println("lhs: " + lhsResult.getKind());
		if(!lhsResult.getKind().equals(rhsResult.getKind()) || 
				(lhsResult.getKind().equals("int") && rhsResult.getKind().equals("double"))) {
			System.out.println(assignOp.getLocation().getRow() + ":" + 
					assignOp.getLocation().getCol() + ":" +
					"Type mismatch");
			passed = false;
			return ErrorType.make();

		}
		
		if(lhsResult.getKind().equals("error") || rhsResult.getKind().equals("error")) {
			return ErrorType.make();
		}		

		assignOp.setType(rhsResult);
		return lhsResult;
	}

	public Type visitLessThanOp(LessThanOp lessThanOp,
			Type lhsResult,
			Type rhsResult)
	{			

		//System.out.println(rhsResult.getKind());
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(lessThanOp.getLocation().getRow() + ":" + 
					lessThanOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}

		return lhsResult;
	}

	public Type visitLessOrEqualOp(LessOrEqualOp lessOrEqualOp,
			Type lhsResult,
			Type rhsResult)
	{
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(lessOrEqualOp.getLocation().getRow() + ":" + 
					lessOrEqualOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");	
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}

		return lhsResult;
	}


	public Type visitGreaterThanOp(GreaterThanOp greaterThanOp,
			Type lhsResult, 
			Type rhsResult)
	{
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(greaterThanOp.getLocation().getRow() + ":" + 
					greaterThanOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("error") || lhsResult.getKind().equals("error")) {
			return ErrorType.make();
		}
		
		// set expression
		greaterThanOp.setType(BoolType.make());

		return greaterThanOp.getType();
	}


	public Type visitGreaterOrEqualOp(GreaterOrEqualOp greaterOrEqualOp,
			Type lhsResult, 
			Type rhsResult)
	{
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(greaterOrEqualOp.getLocation().getRow() + ":" + 
					greaterOrEqualOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");
			passed = false;
			return ErrorType.make();			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}

		return lhsResult;
	}


	public Type visitEqualsOp(EqualsOp equalsOp, 
			Type lhsResult, 
			Type rhsResult)
	{
		
		if(!lhsResult.getKind().equals(rhsResult.getKind()) || 
				(lhsResult.getKind().equals("int") && !rhsResult.getKind().equals("double")) ||
				(lhsResult.getKind().equals("double") && !rhsResult.getKind().equals("int"))) {
			System.out.println(equalsOp.getLocation().getRow() + ":" + 
					equalsOp.getLocation().getCol() + ":" +
					"Type mismatch");	
			return ErrorType.make();
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}


	public Type visitNotEqualsOp(NotEqualsOp notEqualsOp,
			Type lhsResult,
			Type rhsResult)
	{
		
		if(!lhsResult.getKind().equals(rhsResult.getKind()) || 
				(lhsResult.getKind().equals("int") && !rhsResult.getKind().equals("double")) ||
				(lhsResult.getKind().equals("double") && !rhsResult.getKind().equals("int"))) {
			System.out.println(notEqualsOp.getLocation().getRow() + ":" + 
					notEqualsOp.getLocation().getCol() + ":" +
					"Type mismatch");	
			return ErrorType.make();
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}


	public Type visitAddOp(AddOp addOp, Type lhsResult, Type rhsResult)
	{		
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(addOp.getLocation().getRow() + ":" + 
					addOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");	
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	
	}


	public Type visitSubtractOp(SubtractOp subtractOp, 
			Type lhsResult, 
			Type rhsResult)
	{

		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(subtractOp.getLocation().getRow() + ":" + 
					subtractOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");	
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}


	public Type visitMultiplyOp(MultiplyOp multiplyOp, 
			Type lhsResult,
			Type rhsResult)
	{		
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(multiplyOp.getLocation().getRow() + ":" + 
					multiplyOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}


	public Type visitDivideOp(DivideOp divideOp, 
			Type lhsResult, 
			Type rhsResult)
	{
		
		if( !(lhsResult.getKind().equals("int") || lhsResult.getKind().equals("double")) || 
				!(rhsResult.getKind().equals("int") || rhsResult.getKind().equals("double"))) {
			System.out.println(divideOp.getLocation().getRow() + ":" + 
					divideOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");	
			passed = false;
			return ErrorType.make();
			
		}
		
		if(rhsResult.getKind().equals("double") || lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}


	public Type visitModOp(ModOp modOp, Type lhsResult, Type rhsResult)
	{
		

		
		if( !(lhsResult.equals("int")) || !(rhsResult.equals("int") )) {
			System.out.println(modOp.getLocation().getRow() + ":" + 
					modOp.getLocation().getCol() + ":" +
					"Operator can only be applied to numeric types");
			passed = false;
			return ErrorType.make();
			
		}	
		
		if(lhsResult.getKind().equals("error")) {
			return rhsResult;
		}
		
		return lhsResult;
	}



	public Type visitNullLit(NullLit nullLit) {
		nullLit.setType(NullType.make());
		return nullLit.getType();
	}

	public Type visitBooleanLit(BooleanLit booleanLit) {
		booleanLit.setType(BoolType.make());
		return BoolType.make();
	}

	public Type visitStringLit(StringLit stringLit){
		stringLit.setType(StringType.make());
		return stringLit.getType();
	}

	public Type visitIntLit(IntLit intLit) {
		intLit.setType(IntType.make());
		return intLit.getType();
	}

	public Type visitDoubleLit(DoubleLit doubleLit) {
		doubleLit.setType(DoubleType.make());
		return doubleLit.getType();
	}

	public Type visitVarRefExp(VarRefExp varRefExp) {	
		varRefExp.setType(varRefExp.getVariableSymbol().getType());
		//System.out.println(varRefExp.getType().getKind());
		varRefExp.makelvalue();
		return varRefExp.getType();
	}


	public Type visitBooleanTypeNode(BooleanTypeNode boolType) {
		return BoolType.make();
	}

	public Type visitIntTypeNode(IntTypeNode intType) {
		return IntType.make();
	}

	public Type visitDoubleTypeNode(DoubleTypeNode doubleType) {
		return DoubleType.make();
	}

	public Type visitStringTypeNode(StringTypeNode stringType) {
		return StringType.make();
	}

	public Type visitPointerTypeNode(PointerTypeNode pointerType,
			Type innerResult)
	{
		return PointerType.make(innerResult);
	}

	public Type visitArrayTypeNode(ArrayTypeNode arrayType, Type innerResult)
	{
		return ArrayType.make(innerResult, arrayType.getSize());
	}
}
